Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/block-library/src/editor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
@import "./spacer/editor.scss";
@import "./subhead/editor.scss";
@import "./table/editor.scss";
@import "./table-of-contents/editor.scss";
@import "./tag-cloud/editor.scss";
@import "./text-columns/editor.scss";
@import "./verse/editor.scss";
Expand Down
2 changes: 2 additions & 0 deletions packages/block-library/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import * as shortcode from './shortcode';
import * as spacer from './spacer';
import * as subhead from './subhead';
import * as table from './table';
import * as tableOfContents from './table-of-contents';
import * as template from './template';
import * as textColumns from './text-columns';
import * as verse from './verse';
Expand Down Expand Up @@ -119,6 +120,7 @@ export const registerCoreBlocks = () => {
spacer,
subhead,
table,
tableOfContents,
tagCloud,
template,
textColumns,
Expand Down
69 changes: 69 additions & 0 deletions packages/block-library/src/table-of-contents/ListLevel.js
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 }
/>
);
}
4 changes: 4 additions & 0 deletions packages/block-library/src/table-of-contents/block.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "core/table-of-contents",
"category": "common"
}
128 changes: 128 additions & 0 deletions packages/block-library/src/table-of-contents/edit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/**
* Internal dependencies
*/
import * as Utils from './utils';
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 {
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( () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest using withSelect to get the page headings instead of doing a manual subscribe here.

Copy link
Contributor Author

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.

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;
21 changes: 21 additions & 0 deletions packages/block-library/src/table-of-contents/editor.scss
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;
}
}
39 changes: 39 additions & 0 deletions packages/block-library/src/table-of-contents/index.js
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,
};
27 changes: 27 additions & 0 deletions packages/block-library/src/table-of-contents/save.js
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 (
<nav className={ props.className }>
<ListLevel
edit={ false }
attributes={ attributes }
setAttributes={ setAttributes }
>
{ Utils.linearToNestedList( headings ) }
</ListLevel>
</nav>
);
}
Loading