-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Add/table of contents #15426
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 #15426
Changes from 4 commits
5c910e8
d6050ad
225990f
b73a652
75c2e6f
273d31f
ffc8791
4ab2946
de0f085
4331062
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| /** | ||
| * WordPress dependencies | ||
| */ | ||
| import { RichText } from '@wordpress/editor'; | ||
|
|
||
| export default function ListLevel( props ) { | ||
| const { edit, attributes, setAttributes } = props; | ||
| let childnodes = null; | ||
|
|
||
| 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" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import * as Utils from './utils'; | ||
| import ListLevel from './ListLevel'; | ||
|
|
||
| /** | ||
| * WordPress dependencies | ||
| */ | ||
| import { Component } from '@wordpress/element'; | ||
| import { subscribe } from '@wordpress/data'; | ||
| import { | ||
| IconButton, | ||
| Toolbar, | ||
| PanelBody, | ||
| ToggleControl, | ||
| } from '@wordpress/components'; | ||
| import { | ||
| BlockControls, | ||
| InspectorControls, | ||
| } from '@wordpress/editor'; | ||
|
|
||
| class TOCEdit extends Component { | ||
| constructor() { | ||
| super( ...arguments ); | ||
|
|
||
| this.state = { | ||
| wpDataUnsubscribe: null, | ||
| }; | ||
|
|
||
| this.toggleAttribute = this.toggleAttribute.bind( this ); | ||
| this.refresh = this.refresh.bind( this ); | ||
| } | ||
|
|
||
| toggleAttribute( propName ) { | ||
| 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 } ); | ||
| this.setState( { wpDataUnsubscribe } ); | ||
| } | ||
|
|
||
| componentWillUnmount() { | ||
| this.state.wpDataUnsubscribe(); | ||
| } | ||
|
|
||
| componentDidUpdate( prevProps, prevState ) { | ||
| const { attributes, setAttributes } = this.props; | ||
| const pageHeadings = Utils.getPageHeadings(); | ||
| if ( JSON.stringify( pageHeadings ) !== JSON.stringify( prevState.pageHeadings ) ) { | ||
| this.setState( { pageHeadings } ); | ||
| if ( attributes.autosync ) { | ||
| setAttributes( { headings: pageHeadings } ); // this is displayed on the page | ||
| } | ||
| } | ||
| } | ||
|
|
||
| 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</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' } | ||
|
||
| checked={ autosync } | ||
| onChange={ () => { | ||
| if ( ! autosync ) { | ||
| this.refresh(); | ||
| } | ||
| this.toggleAttribute( 'autosync' ); | ||
| } } | ||
| /> | ||
| </PanelBody> | ||
| </InspectorControls> | ||
| } | ||
| <ListLevel | ||
| edit={ true } | ||
| attributes={ attributes } | ||
| setAttributes={ setAttributes } | ||
| > | ||
| { 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; | ||
| } | ||
| } | ||
|
|
||
| ul { | ||
| margin-bottom: 0; | ||
| } | ||
| } | ||
|
|
||
| p { | ||
| opacity: 0.5; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| /** | ||
| * 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.' ), | ||
| 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'; | ||
| 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 ( | ||
| <div className={ props.className }> | ||
| <ListLevel | ||
| edit={ false } | ||
| attributes={ attributes } | ||
| setAttributes={ setAttributes } | ||
| > | ||
| { Utils.linearToNestedList( headings ) } | ||
| </ListLevel> | ||
| </div> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd suggest using
withSelectto get the page headings instead of doing a manual subscribe here.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@swissspidy Thanks for the suggestion. I will definitely look into that.