-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Autogenerate heading anchors #30825
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Autogenerate heading anchors #30825
Changes from all commits
5cb5c9a
ca2f664
556f82b
4a6acd4
042a1aa
8c4be85
766ef32
3296651
3e6928c
1e5d3d7
2d20135
69efbab
e5af9c8
15e04db
635eca9
4c03e1b
abeaaaf
9f728c2
4579a72
93b0d4d
a9f6381
762af2c
c3ccf1c
47014dd
cc34994
4b9e037
a1263d9
370409e
951480c
9953f70
694180e
34d1721
50141d7
7e0f1d9
f1ad7c2
c61d9a7
7d8c3ea
00006fe
7b8b783
516c400
842449d
3ec7e1e
171f228
577feb6
5706771
60114f1
0dbefeb
2a021a6
716b5a9
4f5c06e
88cf66c
8c0e353
234abfc
6535526
8f39085
43cc150
db12f78
5bb9302
bbabee8
98d29dc
b5cb1f9
2b1f215
79f49ff
b5b34a8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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 = {}; | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: I think this is an implementation detail and should be part of the |
||||||
|
|
||||||
| 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; | ||||||
aristath marked this conversation as resolved.
Show resolved
Hide resolved
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
If you decide to move the cache variable will need a new |
||||||
| }, [ 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"> | ||||||
|
|
@@ -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; | ||||||
|
|
||||||
Uh oh!
There was an error while loading. Please reload this page.