diff --git a/packages/block-library/src/heading/autogenerate-anchors.js b/packages/block-library/src/heading/autogenerate-anchors.js new file mode 100644 index 00000000000000..ba8150e6e7e69e --- /dev/null +++ b/packages/block-library/src/heading/autogenerate-anchors.js @@ -0,0 +1,65 @@ +/** + * External dependencies + */ +import { deburr, trim } from 'lodash'; + +/** + * Returns the text without markup. + * + * @param {string} text The text. + * + * @return {string} The text without markup. + */ +const getTextWithoutMarkup = ( text ) => { + const dummyElement = document.createElement( 'div' ); + dummyElement.innerHTML = text; + return dummyElement.innerText; +}; + +/** + * Get the slug from the content. + * + * @param {string} content The block content. + * + * @return {string} Returns the slug. + */ +const getSlug = ( content ) => { + // Get the slug. + return trim( + deburr( getTextWithoutMarkup( content ) ) + .replace( /[^\p{L}\p{N}]+/gu, '-' ) + .toLowerCase(), + '-' + ); +}; + +/** + * Generate the anchor for a heading. + * + * @param {string} clientId The block ID. + * @param {string} content The block content. + * @param {string[]} allHeadingAnchors An array containing all headings anchors. + * + * @return {string|null} Return the heading anchor. + */ +export const generateAnchor = ( clientId, content, allHeadingAnchors ) => { + const slug = getSlug( content ); + // If slug is empty, then return null. + // Returning null instead of an empty string allows us to check again when the content changes. + if ( '' === slug ) { + return null; + } + + delete allHeadingAnchors[ clientId ]; + + let anchor = slug; + let i = 0; + + // If the anchor already exists in another heading, append -i. + while ( Object.values( allHeadingAnchors ).includes( anchor ) ) { + i += 1; + anchor = slug + '-' + i; + } + + return anchor; +}; diff --git a/packages/block-library/src/heading/edit.js b/packages/block-library/src/heading/edit.js index 023184969babd6..9ca81e0410a66b 100644 --- a/packages/block-library/src/heading/edit.js +++ b/packages/block-library/src/heading/edit.js @@ -7,6 +7,7 @@ import classnames from 'classnames'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; +import { useEffect } from '@wordpress/element'; import { createBlock } from '@wordpress/blocks'; import { AlignmentControl, @@ -19,6 +20,9 @@ import { * Internal dependencies */ import HeadingLevelDropdown from './heading-level-dropdown'; +import { generateAnchor } from './autogenerate-anchors'; + +const allHeadingAnchors = {}; function HeadingEdit( { attributes, @@ -28,7 +32,7 @@ function HeadingEdit( { style, clientId, } ) { - const { textAlign, content, level, placeholder } = attributes; + const { textAlign, content, level, placeholder, anchor } = attributes; const tagName = 'h' + level; const blockProps = useBlockProps( { className: classnames( { @@ -37,6 +41,33 @@ function HeadingEdit( { style, } ); + // Initially set anchor for headings that have content but no anchor set. + // This is used when transforming a block to heading, or for legacy anchors. + useEffect( () => { + if ( ! anchor && content ) { + setAttributes( { + anchor: generateAnchor( clientId, content, allHeadingAnchors ), + } ); + } + allHeadingAnchors[ clientId ] = anchor; + }, [ content, anchor ] ); + + const onContentChange = ( value ) => { + const newAttrs = { content: value }; + if ( + ! anchor || + ! value || + generateAnchor( clientId, content, allHeadingAnchors ) === anchor + ) { + newAttrs.anchor = generateAnchor( + clientId, + value, + allHeadingAnchors + ); + } + setAttributes( newAttrs ); + }; + return ( <> @@ -57,7 +88,7 @@ function HeadingEdit( { identifier="content" tagName={ tagName } value={ content } - onChange={ ( value ) => setAttributes( { content: value } ) } + onChange={ onContentChange } onMerge={ mergeBlocks } onSplit={ ( value, isOriginal ) => { let block; diff --git a/packages/e2e-tests/specs/editor/blocks/__snapshots__/heading.test.js.snap b/packages/e2e-tests/specs/editor/blocks/__snapshots__/heading.test.js.snap index c340a475f354a1..290019372feeb3 100644 --- a/packages/e2e-tests/specs/editor/blocks/__snapshots__/heading.test.js.snap +++ b/packages/e2e-tests/specs/editor/blocks/__snapshots__/heading.test.js.snap @@ -2,25 +2,25 @@ exports[`Heading can be created by prefixing existing content with number signs and a space 1`] = ` " -

4

+

4

" `; exports[`Heading can be created by prefixing number sign and a space 1`] = ` " -

3

+

3

" `; exports[`Heading should correctly apply custom colors 1`] = ` " -

Heading

+

Heading

" `; exports[`Heading should correctly apply named colors 1`] = ` " -

Heading

+

Heading

" `; @@ -30,13 +30,13 @@ exports[`Heading should create a paragraph block above when pressing enter at th -

a

+

a

" `; exports[`Heading should create a paragraph block below when pressing enter at the end 1`] = ` " -

a

+

a

@@ -46,12 +46,12 @@ exports[`Heading should create a paragraph block below when pressing enter at th exports[`Heading should not work with the list input rule 1`] = ` " -

1. H

+

1. H

" `; exports[`Heading should work with the format input rules 1`] = ` " -

code

+

code

" `; diff --git a/packages/e2e-tests/specs/editor/blocks/__snapshots__/quote.test.js.snap b/packages/e2e-tests/specs/editor/blocks/__snapshots__/quote.test.js.snap index 57fcc6c61dbdd0..6345efae5ed728 100644 --- a/packages/e2e-tests/specs/editor/blocks/__snapshots__/quote.test.js.snap +++ b/packages/e2e-tests/specs/editor/blocks/__snapshots__/quote.test.js.snap @@ -40,7 +40,7 @@ exports[`Quote can be converted to paragraphs and renders only one paragraph for exports[`Quote can be created by converting a heading 1`] = ` " -

test

+

test

" `; @@ -144,7 +144,7 @@ exports[`Quote can be split in the middle and merged back 4`] = ` exports[`Quote is transformed to a heading and a quote if the quote contains a citation 1`] = ` " -

one

+

one

@@ -154,7 +154,7 @@ exports[`Quote is transformed to a heading and a quote if the quote contains a c exports[`Quote is transformed to a heading and a quote if the quote contains multiple paragraphs 1`] = ` " -

one

+

one

@@ -164,7 +164,7 @@ exports[`Quote is transformed to a heading and a quote if the quote contains mul exports[`Quote is transformed to a heading if the quote just contains one paragraph 1`] = ` " -

one

+

one

" `; @@ -176,7 +176,7 @@ exports[`Quote is transformed to an empty heading if the quote is empty 1`] = ` exports[`Quote the resuling quote after transforming to a heading can be transformed again 1`] = ` " -

one

+

one

@@ -186,11 +186,11 @@ exports[`Quote the resuling quote after transforming to a heading can be transfo exports[`Quote the resuling quote after transforming to a heading can be transformed again 2`] = ` " -

one

+

one

-

two

+

two

@@ -200,14 +200,14 @@ exports[`Quote the resuling quote after transforming to a heading can be transfo exports[`Quote the resuling quote after transforming to a heading can be transformed again 3`] = ` " -

one

+

one

-

two

+

two

-

cite

+

cite

" `; diff --git a/packages/e2e-tests/specs/editor/various/__snapshots__/block-grouping.test.js.snap b/packages/e2e-tests/specs/editor/various/__snapshots__/block-grouping.test.js.snap index f3c24c200b971a..aeaafed4ea8a7a 100644 --- a/packages/e2e-tests/specs/editor/various/__snapshots__/block-grouping.test.js.snap +++ b/packages/e2e-tests/specs/editor/various/__snapshots__/block-grouping.test.js.snap @@ -3,7 +3,7 @@ exports[`Block Grouping Group creation creates a group from multiple blocks of different types via block transforms 1`] = ` "
-

Group Heading

+

Group Heading

@@ -51,7 +51,7 @@ exports[`Block Grouping Group creation creates a group from multiple blocks of t exports[`Block Grouping Group creation groups and ungroups multiple blocks of different types via options toolbar 1`] = ` "
-

Group Heading

+

Group Heading

@@ -66,7 +66,7 @@ exports[`Block Grouping Group creation groups and ungroups multiple blocks of di exports[`Block Grouping Group creation groups and ungroups multiple blocks of different types via options toolbar 2`] = ` " -

Group Heading

+

Group Heading

@@ -81,7 +81,7 @@ exports[`Block Grouping Group creation groups and ungroups multiple blocks of di exports[`Block Grouping Preserving selected blocks attributes preserves width alignment settings of selected blocks 1`] = ` "
-

Group Heading

+

Group Heading

diff --git a/packages/e2e-tests/specs/editor/various/__snapshots__/inserting-blocks.test.js.snap b/packages/e2e-tests/specs/editor/various/__snapshots__/inserting-blocks.test.js.snap index ec465386aaf05d..b97b80ddf59aea 100644 --- a/packages/e2e-tests/specs/editor/various/__snapshots__/inserting-blocks.test.js.snap +++ b/packages/e2e-tests/specs/editor/various/__snapshots__/inserting-blocks.test.js.snap @@ -71,7 +71,7 @@ exports[`Inserting blocks inserts a block in proper place after having clicked \ -

Heading

+

Heading

@@ -93,7 +93,7 @@ exports[`Inserting blocks inserts a block in proper place after having clicked \ -

Heading

+

Heading

diff --git a/packages/e2e-tests/specs/widgets/editing-widgets.test.js b/packages/e2e-tests/specs/widgets/editing-widgets.test.js index ff3a7cb65412f8..2cf7502ebca687 100644 --- a/packages/e2e-tests/specs/widgets/editing-widgets.test.js +++ b/packages/e2e-tests/specs/widgets/editing-widgets.test.js @@ -380,7 +380,7 @@ describe( 'Widgets screen', () => {

First Paragraph

-

My Heading

+

My Heading

Second Paragraph