From 68a36363e538dd3ae4d1e25e387462be0de2059d Mon Sep 17 00:00:00 2001 From: Carolina Nymark Date: Tue, 14 Jan 2025 15:28:02 +0100 Subject: [PATCH 1/3] Document Outline: Add check for the main element --- .../src/components/document-outline/index.js | 74 +++++++++++++++---- .../components/document-outline/style.scss | 3 +- 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/packages/editor/src/components/document-outline/index.js b/packages/editor/src/components/document-outline/index.js index 6c498ccc79990..058201afefd7d 100644 --- a/packages/editor/src/components/document-outline/index.js +++ b/packages/editor/src/components/document-outline/index.js @@ -73,6 +73,15 @@ 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. @@ -95,6 +104,17 @@ const computeOutlineHeadings = ( blocks = [] ) => { } ); }; +const computeOutlineMainElements = ( blocks = [] ) => { + return blocks.flatMap( ( block = {} ) => { + if ( block.attributes.tagName === 'main' ) { + return { + ...block, + }; + } + return computeOutlineMainElements( block.innerBlocks ); + } ); +}; + const isEmptyHeading = ( heading ) => ! heading.attributes.content || heading.attributes.content.trim().length === 0; @@ -129,18 +149,7 @@ export default function DocumentOutline( { 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 mainElements = computeOutlineMainElements( blocks ); // Not great but it's the simplest way to locate the title right now. const titleNode = document.querySelector( '.editor-post-title__input' ); @@ -154,9 +163,48 @@ export default function DocumentOutline( { ); const hasMultipleH1 = countByLevel[ 1 ] > 1; + const classNames = + 'document-outline' + + ( headings.length < 1 ? ' has-no-headings' : '' ) + + ( mainElements.length === 0 ? ' has-no-main' : '' ); + return ( -
+
+ { headings.length < 1 && ( + <> + +

+ { __( + 'Navigate the structure of your document and address issues like empty or incorrect heading levels.' + ) } +

+ + ) } + { 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.' + ) } +

+ ) }
    + { mainElements.map( ( item ) => { + return ( + { + selectBlock( item.clientId ); + onSelect?.(); + } } + > + { mainElements.length !== 1 && incorrectMainTag } + + ); + } ) } { hasTitle && ( svg { margin-top: $grid-unit-30 + $grid-unit-05; } From 123d66d6f93d681f4343703a00d99de07fef4a15 Mon Sep 17 00:00:00 2001 From: Carolina Nymark Date: Thu, 16 Jan 2025 13:33:19 +0100 Subject: [PATCH 2/3] Display nested blocks in the outline --- .../src/components/document-outline/index.js | 283 +++++++++++------- 1 file changed, 171 insertions(+), 112 deletions(-) diff --git a/packages/editor/src/components/document-outline/index.js b/packages/editor/src/components/document-outline/index.js index 058201afefd7d..da3c58b2eed92 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 */ @@ -83,35 +88,40 @@ const incorrectMainTag = [ ]; /** - * 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 ); - } ); -}; -const computeOutlineMainElements = ( blocks = [] ) => { - return blocks.flatMap( ( block = {} ) => { - if ( block.attributes.tagName === 'main' ) { + if ( isMain ) { return { ...block, + type: 'main', + children: computeOutlineElements( + block.innerBlocks, + isEditingTemplate + ), }; } - return computeOutlineMainElements( block.innerBlocks ); + + return computeOutlineElements( block.innerBlocks, isEditingTemplate ); } ); }; @@ -133,43 +143,153 @@ 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 ); - const mainElements = computeOutlineMainElements( blocks ); + 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; - const classNames = - 'document-outline' + - ( headings.length < 1 ? ' has-no-headings' : '' ) + - ( mainElements.length === 0 ? ' has-no-main' : '' ); + const documentOutlineItems = ( { item } ) => { + if ( isEditingTemplate && item.type === 'main' ) { + return ( +
  • + { + selectBlock( item.clientId ); + onSelect?.(); + } } + > + { 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 && ( <> @@ -180,7 +300,7 @@ export default function DocumentOutline( {

    ) } - { mainElements.length === 0 && ( + { 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.' @@ -188,78 +308,17 @@ export default function DocumentOutline( {

    ) }
      - { mainElements.map( ( item ) => { - return ( - { - selectBlock( item.clientId ); - onSelect?.(); - } } - > - { mainElements.length !== 1 && incorrectMainTag } - - ); - } ) } - { hasTitle && ( - - { title } - + { 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 } - - ); - } ) }
    ); From 15767c70f91c20a4584f67aa39b9ed7b276cca40 Mon Sep 17 00:00:00 2001 From: Carolina Nymark Date: Thu, 16 Jan 2025 13:36:13 +0100 Subject: [PATCH 3/3] Do not use the 'has-no-main' class name in the post editor --- packages/editor/src/components/document-outline/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/components/document-outline/index.js b/packages/editor/src/components/document-outline/index.js index da3c58b2eed92..188c6b8c88776 100644 --- a/packages/editor/src/components/document-outline/index.js +++ b/packages/editor/src/components/document-outline/index.js @@ -287,7 +287,7 @@ export default function DocumentOutline( { className={ clsx( 'document-outline', headings.length < 1 && 'has-no-headings', - mainElements.length === 0 && 'has-no-main' + isEditingTemplate && mainElements.length === 0 && 'has-no-main' ) } > { headings.length < 1 && (