Skip to content

Commit

Permalink
Autogenerate heading anchors (#30825)
Browse files Browse the repository at this point in the history
* auto-generate anchors for heading blocks

* Don't use "we" & "us" in comments

* account for non-latin anchors

* remove lodash dependency

* Revert "remove lodash dependency"

This reverts commit bf0517c.

* add dom-ready dependency

* refactor everything

* use useSelect

* This was not removed on purpose

* Move dummyElement inside getTextWithoutMarkup

* Update e2e tests

* Don't use a plain "wp-" as anchor

* typo

* Improve empty string handling

* Improve inline comments

* Bugfix anchor generation for empty headings

* Update packages/block-library/src/heading/edit.js

Co-authored-by: Nik Tsekouras <ntsekouras@outlook.com>

* Use blockEditorStore

* cs

* refactor hook

* Update packages/block-library/src/heading/edit.js

Co-authored-by: Nik Tsekouras <ntsekouras@outlook.com>

* no need for lodash here, these are pretty simple

* refactor - no prefix

* update e2e

* bugfix

* simplify

* fix for block transforms

* Improve generated slugs

* bring back the prefix

* Revert "update e2e"

This reverts commit 792cfad.

* remove wp- prefix if not autogenerated anchor

* fix for empty/null anchors

* use useEffect properly

* Refactor to not use a prefix.

* Maybe not use setAttributes? 🤔

* no need for this condition

* This was too ugly to stay for more than 10 minutes

* remove prefix from tests

* alternative to useEffect

* Revert "alternative to useEffect"

This reverts commit 516c400.

* only watch the content

* mark anchor changes as not persistent

* yet another refactor

* fix for legacy headers & conversions

* Typo was causing a crash

* use useEffect for side effects

* Simplify & improve performance

* Use an object instead of Set

Co-authored-by: Nik Tsekouras <ntsekouras@outlook.com>
  • Loading branch information
aristath and ntsekouras committed Oct 14, 2021
1 parent b6f0bdf commit c906643
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 27 deletions.
65 changes: 65 additions & 0 deletions 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;
};
35 changes: 33 additions & 2 deletions packages/block-library/src/heading/edit.js
Expand Up @@ -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,
Expand All @@ -19,6 +20,9 @@ import {
* Internal dependencies
*/
import HeadingLevelDropdown from './heading-level-dropdown';
import { generateAnchor } from './autogenerate-anchors';

const allHeadingAnchors = {};

function HeadingEdit( {
attributes,
Expand All @@ -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( {
Expand All @@ -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 (
<>
<BlockControls group="block">
Expand All @@ -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;
Expand Down
Expand Up @@ -2,25 +2,25 @@

exports[`Heading can be created by prefixing existing content with number signs and a space 1`] = `
"<!-- wp:heading {\\"level\\":4} -->
<h4>4</h4>
<h4 id=\\"4\\">4</h4>
<!-- /wp:heading -->"
`;
exports[`Heading can be created by prefixing number sign and a space 1`] = `
"<!-- wp:heading {\\"level\\":3} -->
<h3>3</h3>
<h3 id=\\"3\\">3</h3>
<!-- /wp:heading -->"
`;
exports[`Heading should correctly apply custom colors 1`] = `
"<!-- wp:heading {\\"level\\":3,\\"style\\":{\\"color\\":{\\"text\\":\\"#7700ff\\"}}} -->
<h3 class=\\"has-text-color\\" style=\\"color:#7700ff\\">Heading</h3>
<h3 class=\\"has-text-color\\" id=\\"heading\\" style=\\"color:#7700ff\\">Heading</h3>
<!-- /wp:heading -->"
`;
exports[`Heading should correctly apply named colors 1`] = `
"<!-- wp:heading {\\"textColor\\":\\"luminous-vivid-orange\\"} -->
<h2 class=\\"has-luminous-vivid-orange-color has-text-color\\">Heading</h2>
<h2 class=\\"has-luminous-vivid-orange-color has-text-color\\" id=\\"heading\\">Heading</h2>
<!-- /wp:heading -->"
`;
Expand All @@ -30,13 +30,13 @@ exports[`Heading should create a paragraph block above when pressing enter at th
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2>a</h2>
<h2 id=\\"a\\">a</h2>
<!-- /wp:heading -->"
`;
exports[`Heading should create a paragraph block below when pressing enter at the end 1`] = `
"<!-- wp:heading -->
<h2>a</h2>
<h2 id=\\"a\\">a</h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->
Expand All @@ -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`] = `
"<!-- wp:heading -->
<h2>1. H</h2>
<h2 id=\\"1-h\\">1. H</h2>
<!-- /wp:heading -->"
`;
exports[`Heading should work with the format input rules 1`] = `
"<!-- wp:heading -->
<h2><code>code</code></h2>
<h2 id=\\"code\\"><code>code</code></h2>
<!-- /wp:heading -->"
`;
Expand Up @@ -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`] = `
"<!-- wp:quote -->
<blockquote class=\\"wp-block-quote\\"><p>test</p></blockquote>
<blockquote class=\\"wp-block-quote\\" id=\\"test\\"><p>test</p></blockquote>
<!-- /wp:quote -->"
`;
Expand Down Expand Up @@ -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`] = `
"<!-- wp:heading -->
<h2>one</h2>
<h2 id=\\"one\\">one</h2>
<!-- /wp:heading -->
<!-- wp:quote -->
Expand All @@ -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`] = `
"<!-- wp:heading -->
<h2>one</h2>
<h2 id=\\"one\\">one</h2>
<!-- /wp:heading -->
<!-- wp:quote -->
Expand All @@ -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`] = `
"<!-- wp:heading -->
<h2>one</h2>
<h2 id=\\"one\\">one</h2>
<!-- /wp:heading -->"
`;
Expand All @@ -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`] = `
"<!-- wp:heading -->
<h2>one</h2>
<h2 id=\\"one\\">one</h2>
<!-- /wp:heading -->
<!-- wp:quote -->
Expand All @@ -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`] = `
"<!-- wp:heading -->
<h2>one</h2>
<h2 id=\\"one\\">one</h2>
<!-- /wp:heading -->
<!-- wp:heading -->
<h2>two</h2>
<h2 id=\\"two\\">two</h2>
<!-- /wp:heading -->
<!-- wp:quote -->
Expand All @@ -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`] = `
"<!-- wp:heading -->
<h2>one</h2>
<h2 id=\\"one\\">one</h2>
<!-- /wp:heading -->
<!-- wp:heading -->
<h2>two</h2>
<h2 id=\\"two\\">two</h2>
<!-- /wp:heading -->
<!-- wp:heading -->
<h2>cite</h2>
<h2 id=\\"cite\\">cite</h2>
<!-- /wp:heading -->"
`;
Expand Up @@ -3,7 +3,7 @@
exports[`Block Grouping Group creation creates a group from multiple blocks of different types via block transforms 1`] = `
"<!-- wp:group -->
<div class=\\"wp-block-group\\"><!-- wp:heading -->
<h2>Group Heading</h2>
<h2 id=\\"group-heading\\">Group Heading</h2>
<!-- /wp:heading -->
<!-- wp:image -->
Expand Down Expand Up @@ -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`] = `
"<!-- wp:group -->
<div class=\\"wp-block-group\\"><!-- wp:heading -->
<h2>Group Heading</h2>
<h2 id=\\"group-heading\\">Group Heading</h2>
<!-- /wp:heading -->
<!-- wp:image -->
Expand All @@ -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`] = `
"<!-- wp:heading -->
<h2>Group Heading</h2>
<h2 id=\\"group-heading\\">Group Heading</h2>
<!-- /wp:heading -->
<!-- wp:image -->
Expand All @@ -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`] = `
"<!-- wp:group {\\"align\\":\\"full\\"} -->
<div class=\\"wp-block-group alignfull\\"><!-- wp:heading -->
<h2>Group Heading</h2>
<h2 id=\\"group-heading\\">Group Heading</h2>
<!-- /wp:heading -->
<!-- wp:image {\\"align\\":\\"full\\"} -->
Expand Down
Expand Up @@ -71,7 +71,7 @@ exports[`Inserting blocks inserts a block in proper place after having clicked \
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2>Heading</h2>
<h2 id=\\"heading\\">Heading</h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->
Expand All @@ -93,7 +93,7 @@ exports[`Inserting blocks inserts a block in proper place after having clicked \
<!-- /wp:cover -->
<!-- wp:heading -->
<h2>Heading</h2>
<h2 id=\\"heading\\">Heading</h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->
Expand Down
2 changes: 1 addition & 1 deletion packages/e2e-tests/specs/widgets/editing-widgets.test.js
Expand Up @@ -380,7 +380,7 @@ describe( 'Widgets screen', () => {
<p>First Paragraph</p>
</div></div>
<div class=\\"widget widget_block\\"><div class=\\"widget-content\\">
<h2>My Heading</h2>
<h2 id=\\"my-heading\\">My Heading</h2>
</div></div>
<div class=\\"widget widget_block widget_text\\"><div class=\\"widget-content\\">
<p>Second Paragraph</p>
Expand Down

0 comments on commit c906643

Please sign in to comment.