-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Add Table of Contents block #21040
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
Add Table of Contents block #21040
Changes from 1 commit
e5bea61
ba40205
ba5c43a
8507968
50df5b5
4c650b5
371a901
d2e929b
c0f2e1a
4fce9a1
cb3e495
bcfc383
f6aeb36
366d7bd
0648776
29e3c05
5cfade2
b89368d
b82a9e5
7e4a236
10d49be
32a376a
e49f28d
becb1a9
e7f3dae
8cbe8fc
36adc5b
45ee5f7
04e0df2
afacffc
796900a
d31534c
6d1d82c
a96a5dc
00255a0
af0f900
9c73d7b
99bbe0b
d9037e1
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,74 @@ | ||
| /** | ||
| * WordPress dependencies | ||
| */ | ||
| import { RichText } from '@wordpress/editor'; | ||
|
|
||
| export default function ListLevel( props ) { | ||
| const { edit, attributes, setAttributes } = props; | ||
| let childnodes = null; | ||
SeanDS marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| if ( props.children ) { | ||
| childnodes = props.children.map( function( childnode ) { | ||
| const link = getLinkElement( childnode, props ); | ||
|
|
||
| return ( | ||
| <li key={ childnode.block.anchor }> | ||
| { link } | ||
| { childnode.children ? ( | ||
| <ListLevel | ||
| edit={ edit } | ||
| attributes={ attributes } | ||
| setAttributes={ setAttributes } | ||
| > | ||
| { childnode.children } | ||
| </ListLevel> | ||
| ) : null } | ||
| </li> | ||
| ); | ||
| } ); | ||
|
|
||
| return <ul>{ childnodes }</ul>; | ||
| } | ||
| } | ||
|
|
||
| function getLinkElement( childnode, props ) { | ||
| const { edit, attributes, setAttributes } = props; | ||
| const { headings, autosync } = attributes; | ||
|
|
||
| const updateHeading = ( content ) => { | ||
| headings[ childnode.index ].content = content; | ||
| setAttributes( { headings } ); | ||
| }; | ||
|
|
||
| if ( autosync ) { | ||
| return ( | ||
| <a | ||
| href={ childnode.block.anchor } | ||
| data-level={ childnode.block.level } | ||
| > | ||
| { childnode.block.content } | ||
| </a> | ||
| ); | ||
| } | ||
|
|
||
| if ( edit ) { | ||
| return ( | ||
| <RichText | ||
| tagName="a" | ||
| href={ childnode.block.anchor } | ||
| data-level={ childnode.block.level } | ||
| onChange={ ( content ) => updateHeading( content ) } | ||
| value={ childnode.block.content } | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <RichText.Content | ||
| tagName="a" | ||
| href={ childnode.block.anchor } | ||
| data-level={ childnode.block.level } | ||
| value={ childnode.block.content } | ||
| /> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| { | ||
| "name": "core/table-of-contents", | ||
| "category": "common" | ||
| } | ||
SeanDS marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import * as Utils from './utils'; | ||
SeanDS marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| import ListLevel from './ListLevel'; | ||
|
|
||
| /** | ||
| * WordPress dependencies | ||
| */ | ||
| import { Component } from '@wordpress/element'; | ||
| import { subscribe } from '@wordpress/data'; | ||
| import { __ } from '@wordpress/i18n'; | ||
| import { | ||
| IconButton, | ||
| Toolbar, | ||
| PanelBody, | ||
| ToggleControl, | ||
| } from '@wordpress/components'; | ||
| import { BlockControls, InspectorControls } from '@wordpress/editor'; | ||
|
|
||
| class TOCEdit extends Component { | ||
SeanDS marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| constructor() { | ||
| super( ...arguments ); | ||
|
|
||
| this.state = { | ||
| wpDataUnsubscribe: null, | ||
| }; | ||
|
|
||
| this.toggleAttribute = this.toggleAttribute.bind( this ); | ||
| this.refresh = this.refresh.bind( this ); | ||
| } | ||
|
|
||
| toggleAttribute( propName ) { | ||
SeanDS marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| const value = this.props.attributes[ propName ]; | ||
| const { setAttributes } = this.props; | ||
|
|
||
| setAttributes( { [ propName ]: ! value } ); | ||
| } | ||
|
|
||
| refresh() { | ||
| const { setAttributes } = this.props; | ||
| const headings = Utils.getPageHeadings(); | ||
|
||
| setAttributes( { headings } ); | ||
| } | ||
|
|
||
| componentDidMount() { | ||
| const { attributes, setAttributes } = this.props; | ||
| const headings = attributes.headings || []; | ||
| const wpDataUnsubscribe = subscribe( () => { | ||
|
||
| const pageHeadings = Utils.getPageHeadings(); | ||
| this.setState( { pageHeadings } ); | ||
| } ); | ||
|
|
||
| setAttributes( { headings } ); | ||
|
Contributor
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. I'm a bit unclear what this does. It seems to set the headings attribute to the same value it already is. Might be possible to delete it.
Contributor
Author
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. I think I had to keep that there to make the block work when newly created, but I haven't checked again since changing a bunch of other things. I can experiment removing it to see if it still behaves. |
||
| this.setState( { wpDataUnsubscribe } ); | ||
SeanDS marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| componentWillUnmount() { | ||
| this.state.wpDataUnsubscribe(); | ||
| } | ||
|
|
||
| componentDidUpdate( prevProps, prevState ) { | ||
|
Contributor
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. I'm not completely sure about the need to use
Contributor
Author
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. This is no longer updating state so (I think?) doesn't trigger a re-render. It might still be possible to merge into |
||
| const { attributes, setAttributes } = this.props; | ||
| const pageHeadings = Utils.getPageHeadings(); | ||
SeanDS marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if ( | ||
| JSON.stringify( pageHeadings ) !== | ||
| JSON.stringify( prevState.pageHeadings ) | ||
| ) { | ||
SeanDS marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| this.setState( { pageHeadings } ); | ||
SeanDS marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if ( attributes.autosync ) { | ||
| setAttributes( { headings: pageHeadings } ); // this is displayed on the page | ||
SeanDS marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| } | ||
|
|
||
| render() { | ||
| const { attributes, setAttributes } = this.props; | ||
| const { autosync } = attributes; | ||
| const headings = attributes.headings || []; | ||
| if ( headings.length === 0 ) { | ||
| return ( | ||
| <p> | ||
| { __( | ||
| 'Start adding headings to generate Table of Contents' | ||
SeanDS marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ) } | ||
| </p> | ||
| ); | ||
| } | ||
|
|
||
| Utils.updateHeadingBlockAnchors(); | ||
|
|
||
| return ( | ||
| <div className={ this.props.className }> | ||
| { ! autosync && ( | ||
| <BlockControls> | ||
| <Toolbar> | ||
| <IconButton | ||
| label={ __( 'Update' ) } | ||
| aria-pressed={ this.state.isEditing } | ||
| onClick={ this.refresh } | ||
| icon="update" | ||
| /> | ||
| </Toolbar> | ||
| </BlockControls> | ||
| ) } | ||
| { | ||
| <InspectorControls> | ||
| <PanelBody title={ __( 'Table of Contents Settings' ) }> | ||
| <ToggleControl | ||
| label={ __( 'Auto Sync' ) } | ||
SeanDS marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| checked={ autosync } | ||
| onChange={ () => { | ||
| if ( ! autosync ) { | ||
| this.refresh(); | ||
| } | ||
| this.toggleAttribute( 'autosync' ); | ||
| } } | ||
| /> | ||
| </PanelBody> | ||
| </InspectorControls> | ||
| } | ||
| <ListLevel | ||
| edit={ true } | ||
| attributes={ attributes } | ||
SeanDS marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| setAttributes={ setAttributes } | ||
SeanDS marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| > | ||
| { Utils.linearToNestedList( headings ) } | ||
| </ListLevel> | ||
| </div> | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| export default TOCEdit; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| .wp-block-table-of-contents { | ||
| .editor-block-list__block & ul { | ||
| padding-left: 1.3em; | ||
|
|
||
| a { | ||
| display: block; | ||
|
|
||
| &:focus { | ||
| box-shadow: none; | ||
| } | ||
| } | ||
SeanDS marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| ul { | ||
| margin-bottom: 0; | ||
| } | ||
| } | ||
|
|
||
| p { | ||
| opacity: 0.5; | ||
SeanDS marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| /** | ||
| * WordPress dependencies | ||
| */ | ||
| import { __ } from '@wordpress/i18n'; | ||
|
|
||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import edit from './edit'; | ||
| import metadata from './block.json'; | ||
| import save from './save'; | ||
|
|
||
| const { name } = metadata; | ||
|
|
||
| export { metadata, name }; | ||
|
|
||
| export const settings = { | ||
| title: __( 'Table of Contents' ), | ||
| description: __( | ||
| 'Add a list of internal links allowing your readers to quickly navigate around.' | ||
|
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. I think this description needs to be updated since the block no longer automatically adds anchors to the headings.
Contributor
Author
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. True. Maybe "Add a list of headers allowing your readers to quickly navigate through your post." instead, and somewhere else make it obvious to users how to make them links (maybe with popovers or similar)?
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. I'd say "headings" rather than "headers". Also, the contributor guide recommends avoiding describing features as "allowing" something. How's this?
You might be able to leave off "automatically". |
||
| ), | ||
| icon: 'list-view', | ||
| category: 'layout', | ||
| attributes: { | ||
| headings: { | ||
| source: 'query', | ||
| selector: 'a', | ||
| query: { | ||
| content: { source: 'text' }, | ||
| anchor: { source: 'attribute', attribute: 'href' }, | ||
| level: { source: 'attribute', attribute: 'data-level' }, | ||
| }, | ||
| }, | ||
| autosync: { | ||
| type: 'boolean', | ||
| default: true, | ||
| }, | ||
| }, | ||
| edit, | ||
| save, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import * as Utils from './utils'; | ||
SeanDS marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| import ListLevel from './ListLevel'; | ||
|
|
||
| export default function save( props ) { | ||
| const { attributes, setAttributes } = props; | ||
| const headings = attributes.headings; | ||
|
|
||
| if ( headings.length === 0 ) { | ||
| return null; | ||
| } | ||
|
|
||
| Utils.updateHeadingBlockAnchors(); | ||
|
||
| return ( | ||
| <nav className={ props.className }> | ||
| <ListLevel | ||
| edit={ false } | ||
| attributes={ attributes } | ||
| setAttributes={ setAttributes } | ||
| > | ||
| { Utils.linearToNestedList( headings ) } | ||
| </ListLevel> | ||
| </nav> | ||
| ); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.